﻿using OpenTK;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Windows.Forms;
using LightCAD.MathLib;


namespace LightCAD.Three
{

    public class TransformControls : Object3D
    {
        private struct Unit
        {
            public Vector3 X;
            public Vector3 Y;
            public Vector3 Z;

            public new Vector3 this[string axis]
            {
                get
                {
                    switch (axis)
                    {
                        case "X":
                            return this.X;
                        case "Y":
                            return this.Y;
                        case "Z":
                            return this.Z;
                    }
                    return null;
                }
                set
                {
                    switch (axis)
                    {
                        case "X":
                            this.X = value;
                            break;
                        case "Y":
                            this.Y = value;
                            break;
                        case "Z":
                            this.Z = value;
                            break;
                    }
                }
            }
        }

        private struct Pointer
        {
            public double x;
            public double y;
            public MouseButtons button;
            public Vector2 ToV2()
            {
                return new Vector2(x, y);
            }
        }

        private static Euler _tempEuler = new Euler();
        private static Vector3 _alignVector = new Vector3(0, 1, 0);
        private static Vector3 _zeroVector = new Vector3(0, 0, 0);
        private static Matrix4 _lookAtMatrix = new Matrix4();
        private static Quaternion _tempQuaternion2 = new Quaternion();
        private static Quaternion _identityQuaternion = new Quaternion();
        private static Vector3 _dirVector = new Vector3();
        private static Matrix4 _tempMatrix = new Matrix4();

        private static Vector3 _unitX = new Vector3(1, 0, 0);
        private static Vector3 _unitY = new Vector3(0, 1, 0);
        private static Vector3 _unitZ = new Vector3(0, 0, 1);

        private static Vector3 _v1 = new Vector3();
        private static Vector3 _v2 = new Vector3();
        private static Vector3 _v3 = new Vector3();

        public class TransformControlsGizmo : Object3D
        {
            private struct ControlInfo
            {
                public Object3D obj3d;
                public double[] position;
                public double[] rotation;
                public double[] scale;
                public string tag;
                public ControlInfo(Object3D obj3d, double[] position = null, double[] rotation = null, double[] scale = null, string tag = null)
                {
                    this.obj3d = obj3d;
                    this.position = position;
                    this.rotation = rotation;
                    this.scale = scale;
                    this.tag = tag;
                }
            }

            public JsObj<string, Object3D> gizmo;
            public JsObj<string, Object3D> picker;
            public JsObj<string, Object3D> helper;
            public string mode;
            public string space;
            public Quaternion worldQuaternion;
            public Quaternion worldQuaternionStart;
            public Vector3 worldPosition;
            public Vector3 worldPositionStart;
            public Camera camera;
            public Vector3 cameraPosition;
            public double size;
            public string axis;
            public Vector3 eye;
            public Vector3 rotationAxis;
            public bool dragging;

            public bool showX;
            public bool showY;
            public bool showZ;

            public bool enabled;
            //public Object3D obj;
            //public object translationSnap;
            //public object rotationSnap;
            //public object scaleSnap;

            public TransformControlsGizmo() : base()
            {
                this.type = "TransformControlsGizmo";

                // shared materials

                var gizmoMaterial = new MeshBasicMaterial
                {
                    depthTest = false,
                    depthWrite = false,
                    fog = false,
                    toneMapped = false,
                    transparent = true
                };

                var gizmoLineMaterial = new LineBasicMaterial
                {
                    depthTest = false,
                    depthWrite = false,
                    fog = false,
                    toneMapped = false,
                    transparent = true
                };

                // Make unique material for each axis/color

                var matInvisible = gizmoMaterial.clone();
                matInvisible.opacity = 0.15;

                var matHelper = gizmoLineMaterial.clone();
                matHelper.opacity = 0.5;

                var matRed = gizmoMaterial.clone();
                matRed.color.SetHex(0xff0000);

                var matGreen = gizmoMaterial.clone();
                matGreen.color.SetHex(0x00ff00);

                var matBlue = gizmoMaterial.clone();
                matBlue.color.SetHex(0x0000ff);

                var matRedTransparent = gizmoMaterial.clone();
                matRedTransparent.color.SetHex(0xff0000);
                matRedTransparent.opacity = 0.5;

                var matGreenTransparent = gizmoMaterial.clone();
                matGreenTransparent.color.SetHex(0x00ff00);
                matGreenTransparent.opacity = 0.5;

                var matBlueTransparent = gizmoMaterial.clone();
                matBlueTransparent.color.SetHex(0x0000ff);
                matBlueTransparent.opacity = 0.5;

                var matWhiteTransparent = gizmoMaterial.clone();
                matWhiteTransparent.opacity = 0.25;

                var matYellowTransparent = gizmoMaterial.clone();
                matYellowTransparent.color.SetHex(0xffff00);
                matYellowTransparent.opacity = 0.25;

                var matYellow = gizmoMaterial.clone();
                matYellow.color.SetHex(0xffff00);

                var matGray = gizmoMaterial.clone();
                matGray.color.SetHex(0x787878);

                // reusable geometry

                var arrowGeometry = new CylinderGeometry(0, 0.04, 0.1, 12);
                arrowGeometry.translate(0, 0.05, 0);

                var scaleHandleGeometry = new BoxGeometry(0.08, 0.08, 0.08);
                scaleHandleGeometry.translate(0, 0.04, 0);

                var lineGeometry = new BufferGeometry();
                lineGeometry.setAttribute("position", new Float32BufferAttribute(new double[] { 0, 0, 0, 1, 0, 0 }, 3));

                var lineGeometry2 = new CylinderGeometry(0.0075, 0.0075, 0.5, 3);
                lineGeometry2.translate(0, 0.25, 0);

                TorusGeometry CircleGeometry(double radius, double arc)
                {
                    var geometry = new TorusGeometry(radius, 0.0075, 3, 64, arc * Math.PI * 2);
                    geometry.rotateY(Math.PI / 2);
                    geometry.rotateX(Math.PI / 2);
                    return geometry;
                }

                // Special geometry for transform helper. If scaled with position vector it spans from [0,0,0] to position
                BufferGeometry TranslateHelperGeometry()
                {
                    var geometry = new BufferGeometry();
                    geometry.setAttribute("position", new Float32BufferAttribute(new double[] { 0, 0, 0, 1, 1, 1 }, 3));
                    return geometry;
                }

                // Gizmo definitions - custom hierarchy definitions for setupGizmo() function

                var gizmoTranslate = new JsObj<string, ListEx<ControlInfo>>
                    {
                        { "X", new ListEx<ControlInfo>
                            {
                                new ControlInfo(new Mesh(arrowGeometry, matRed ), new double[]{ 0.5, 0, 0 }, new double[]{ 0, 0, - Math.PI / 2 }),
                                new ControlInfo(new Mesh(arrowGeometry, matRed ), new double[]{ -0.5, 0, 0  }, new double[]{ 0, 0, Math.PI / 2 }),
                                new ControlInfo(new Mesh(lineGeometry2, matRed ), new double[]{ 0, 0, 0   }, new double[]{ 0, 0, - Math.PI / 2 }),
                            }
                        },
                        { "Y", new ListEx<ControlInfo>
                            {
                                new ControlInfo(new Mesh(arrowGeometry, matGreen), new double[]{ 0, 0.5, 0 }, null),
                                new ControlInfo(new Mesh(arrowGeometry, matGreen), new double[]{ 0, -0.5, 0 }, new double[]{Math.PI, 0, 0 }),
                                new ControlInfo(new Mesh(lineGeometry2, matGreen), null, null)
                            }
                        },
                        {"Z", new ListEx<ControlInfo>
                            {
                                new ControlInfo(new Mesh(arrowGeometry, matBlue), new double[]{ 0, 0, 0.5 }, new double[]{ Math.PI / 2, 0, 0 } ),
                                new ControlInfo(new Mesh(arrowGeometry, matBlue), new double[]{ 0, 0, -0.5 }, new double[]{ -Math.PI / 2, 0, 0 } ),
                                new ControlInfo(new Mesh(lineGeometry2, matBlue), null, new double[]{ Math.PI / 2, 0, 0 } )
                            }
                        },
                        { "XYZ", new ListEx<ControlInfo>
                            {
                                new ControlInfo( new Mesh(new OctahedronGeometry(0.1, 0), matWhiteTransparent.clone()), new double[]{ 0, 0, 0 }, null )
                            }
                        },
                        { "XY", new ListEx<ControlInfo>
                            {
                                new ControlInfo(new Mesh(new BoxGeometry(0.15, 0.15, 0.01), matBlueTransparent.clone()), new double[]{ 0.15, 0.15, 0 }, null )
                            }
                        },
                        { "YZ", new ListEx<ControlInfo>
                            {
                                new ControlInfo(new Mesh(new BoxGeometry(0.15, 0.15, 0.01), matRedTransparent.clone()), new double[]{ 0, 0.15, 0.15 }, new double[]{ 0, Math.PI / 2, 0 } )
                            }
                        },
                        { "XZ",new ListEx<ControlInfo>
                            {
                                new ControlInfo( new Mesh(new BoxGeometry(0.15, 0.15, 0.01), matGreenTransparent.clone()), new double[]{ 0.15, 0, 0.15 }, new double[]{ -Math.PI / 2, 0, 0 } )
                            }
                        }
                    };

                var pickerTranslate = new JsObj<string, ListEx<ControlInfo>>
                    {
                        { "X", new ListEx<ControlInfo>
                            {
                                new ControlInfo( new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), new double[]{ 0.3, 0, 0 }, new double[]{ 0, 0, - Math.PI / 2 }),
                                new ControlInfo( new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), new double[]{ - 0.3, 0, 0 }, new double[]{ 0, 0, Math.PI / 2 })
                            }
                        },
                        { "Y", new ListEx<ControlInfo>
                            {
                                new ControlInfo( new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), new double[]{ 0, 0.3, 0 }, null),
                                new ControlInfo( new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), new double[]{ 0, - 0.3, 0 }, new double[]{ 0, 0, Math.PI })
                            }
                        },
                        { "Z", new ListEx<ControlInfo>
                            {
                                new ControlInfo( new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), new double[]{ 0, 0, 0.3 }, new double[]{Math.PI / 2, 0, 0 }),
                                new ControlInfo( new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), new double[]{ 0, 0, - 0.3 }, new double[]{ - Math.PI / 2, 0, 0 })
                            }
                        },
                        { "XYZ", new ListEx<ControlInfo>
                            {
                                new ControlInfo( new Mesh( new OctahedronGeometry( 0.2, 0 ), matInvisible ), null, null )
                            }
                        },
                        { "XY", new ListEx<ControlInfo>
                            {
                                new ControlInfo( new Mesh( new BoxGeometry( 0.2, 0.2, 0.01 ), matInvisible ), new double[]{ 0.15, 0.15, 0 }, null)
                            }
                        },
                        { "YZ", new ListEx<ControlInfo>
                            {
                                new ControlInfo( new Mesh( new BoxGeometry( 0.2, 0.2, 0.01 ), matInvisible ), new double[]{ 0, 0.15, 0.15 }, new double[]{ 0, Math.PI / 2, 0 })
                            }
                        },
                        { "XZ", new ListEx<ControlInfo>
                            {
                                new ControlInfo( new Mesh( new BoxGeometry( 0.2, 0.2, 0.01 ), matInvisible ), new double[]{ 0.15, 0, 0.15 }, new double[]{ - Math.PI / 2, 0, 0 })
                            }
                        }
                    };

                var helperTranslate = new JsObj<string, ListEx<ControlInfo>>
                    {
                        { "START", new ListEx<ControlInfo>
                            {
                                new ControlInfo( new Mesh(new OctahedronGeometry(0.01, 2), matHelper), null, null, null, "helper" )
                            }
                        },
                        { "END", new ListEx<ControlInfo>
                            {
                                new ControlInfo( new Mesh(new OctahedronGeometry(0.01, 2), matHelper), null, null, null, "helper" )
                            }
                        },
                        { "DELTA", new ListEx<ControlInfo>
                            {
                                new ControlInfo(new Line(TranslateHelperGeometry(), matHelper), null, null, null, "helper" )
                            }
                        },
                        { "X", new ListEx<ControlInfo>
                            {
                                new ControlInfo( new Line(lineGeometry, matHelper.clone()), new double[]{ -1e3, 0, 0 }, null, new double[]{ 1e6, 1, 1 }, "helper" )
                            }
                        },
                        { "Y", new ListEx<ControlInfo>
                            {
                                new ControlInfo( new Line(lineGeometry, matHelper.clone()), new double[]{ 0, -1e3, 0 }, new double[]{ 0, 0, Math.PI / 2 }, new double[] { 1e6, 1, 1 }, "helper" )
                            }
                        },
                        { "Z", new ListEx<ControlInfo>
                            {
                                new ControlInfo( new Line(lineGeometry, matHelper.clone()), new double[]{ 0, 0, -1e3 }, new double[]{ 0, -Math.PI / 2, 0 }, new double[]{ 1e6, 1, 1 }, "helper" )
                            }
                        }
                    };

                var gizmoRotate = new JsObj<string, ListEx<ControlInfo>>
                    {
                        { "XYZE", new ListEx<ControlInfo>
                            {
                                new ControlInfo( new Mesh(CircleGeometry(0.5, 1), matGray), null, new double[] { 0, Math.PI / 2, 0 })
                            }
                        },
                        { "X", new ListEx<ControlInfo>
                            {
                                new ControlInfo( new Mesh(CircleGeometry(0.5, 0.5), matRed) )
                            }
                        },
                        { "Y", new ListEx<ControlInfo>
                            {
                                new ControlInfo( new Mesh(CircleGeometry(0.5, 0.5), matGreen), null, new double[] { 0, 0, - Math.PI / 2 })
                            }
                        },
                        { "Z", new ListEx<ControlInfo>
                            {
                                new ControlInfo( new Mesh(CircleGeometry(0.5, 0.5), matBlue), null, new double[] { 0, Math.PI / 2, 0 })
                            }
                        },
                        { "E", new ListEx<ControlInfo>
                            {
                                new ControlInfo( new Mesh(CircleGeometry(0.75, 1), matYellowTransparent), null, new double[] { 0, Math.PI / 2, 0 })
                            }
                        }
                    };

                var helperRotate = new JsObj<string, ListEx<ControlInfo>>
                    {
                        { "AXIS", new ListEx<ControlInfo>
                            {
                                new ControlInfo( new Line(lineGeometry, matHelper.clone()), new double[]{ - 1e3, 0, 0 }, null, new double[] { 1e6, 1, 1 }, "helper" )
                            }
                        }
                    };

                var pickerRotate = new JsObj<string, ListEx<ControlInfo>>
                    {
                        { "XYZE", new ListEx<ControlInfo>
                            {
                                new ControlInfo(new Mesh(new SphereGeometry(0.25, 10, 8), matInvisible) )
                            }
                        },
                        { "X", new ListEx<ControlInfo>
                            {
                                new ControlInfo( new Mesh(new TorusGeometry(0.5, 0.1, 4, 24), matInvisible), new double[]{ 0, 0, 0 }, new double[]{ 0, - Math.PI / 2, - Math.PI / 2 })
                            }
                        },
                        { "Y", new ListEx<ControlInfo>
                            {
                                new ControlInfo( new Mesh(new TorusGeometry(0.5, 0.1, 4, 24), matInvisible), new double[]{ 0, 0, 0 }, new double[]{Math.PI / 2, 0, 0 })
                            }
                        },
                        { "Z", new ListEx<ControlInfo>
                            {
                                new ControlInfo(  new Mesh(new TorusGeometry(0.5, 0.1, 4, 24), matInvisible), new double[] { 0, 0, 0 }, new double[] { 0, 0, - Math.PI / 2 }),
                            }
                        },
                        { "E", new ListEx<ControlInfo>
                            {
                                new ControlInfo(  new Mesh(new TorusGeometry(0.75, 0.1, 2, 24), matInvisible) )
                            }
                        }
                    };

                var gizmoScale = new JsObj<string, ListEx<ControlInfo>>
                    {
                        { "X", new ListEx<ControlInfo>
                            {
                                new ControlInfo( new Mesh(scaleHandleGeometry, matRed), new double[]{ 0.5, 0, 0 }, new double[]{ 0, 0, - Math.PI / 2 }),
                                new ControlInfo( new Mesh(lineGeometry2, matRed), new double[]{ 0, 0, 0 }, new double[]{ 0, 0, - Math.PI / 2 }),
                                new ControlInfo( new Mesh(scaleHandleGeometry, matRed), new double[]{ - 0.5, 0, 0 }, new double[]{ 0, 0, Math.PI / 2 }),
                            }
                        },
                        { "Y", new ListEx<ControlInfo>
                            {
                                new ControlInfo( new Mesh(scaleHandleGeometry, matGreen), new double[]{ 0, 0.5, 0 }),
                                new ControlInfo( new Mesh(lineGeometry2, matGreen) ),
                                new ControlInfo( new Mesh(scaleHandleGeometry, matGreen), new double[]{ 0, - 0.5, 0 }, new double[]{ 0, 0, Math.PI}),
                            }
                        },
                        { "Z", new ListEx<ControlInfo>
                            {
                                new ControlInfo( new Mesh(scaleHandleGeometry, matBlue), new double[]{ 0, 0, 0.5 }, new double[]{Math.PI / 2, 0, 0 }),
                                new ControlInfo( new Mesh(lineGeometry2, matBlue), new double[]{ 0, 0, 0 }, new double[]{Math.PI / 2, 0, 0 }),
                                new ControlInfo( new Mesh(scaleHandleGeometry, matBlue), new double[]{ 0, 0, - 0.5 }, new double[]{ - Math.PI / 2, 0, 0 })
                            }
                        },
                        { "XY", new ListEx<ControlInfo>
                            {
                                new ControlInfo( new Mesh(new BoxGeometry(0.15, 0.15, 0.01), matBlueTransparent), new double[]{ 0.15, 0.15, 0 })
                            }
                        },
                        { "YZ", new ListEx<ControlInfo>
                            {
                                new ControlInfo( new Mesh(new BoxGeometry(0.15, 0.15, 0.01), matRedTransparent), new double[]{ 0, 0.15, 0.15 }, new double[]{ 0, Math.PI / 2, 0 })
                            }
                        },
                        { "XZ", new ListEx<ControlInfo>
                            {
                                new ControlInfo( new Mesh(new BoxGeometry(0.15, 0.15, 0.01), matGreenTransparent), new double[]{ 0.15, 0, 0.15 }, new double[]{ - Math.PI / 2, 0, 0 })
                            }
                        },
                        { "XYZ", new ListEx<ControlInfo>
                            {
                                new ControlInfo( new Mesh(new BoxGeometry(0.1, 0.1, 0.1), matWhiteTransparent.clone()) ),
                            }
                        }
                    };

                var pickerScale = new JsObj<string, ListEx<ControlInfo>>
                    {
                        {"X", new ListEx<ControlInfo>
                            {
                                new ControlInfo( new Mesh(new CylinderGeometry(0.2, 0, 0.6, 4), matInvisible), new double[]{ 0.3, 0, 0 }, new double[]{ 0, 0, - Math.PI / 2 }),
                                new ControlInfo(new Mesh(new CylinderGeometry(0.2, 0, 0.6, 4), matInvisible), new double[]{ -0.3, 0, 0 }, new double[]{ 0, 0, Math.PI / 2 })
                            }
                        },
                        {"Y", new ListEx<ControlInfo>
                            {
                                new ControlInfo( new Mesh(new CylinderGeometry(0.2, 0, 0.6, 4), matInvisible), new double[]{ 0, 0.3, 0 }),
                                new ControlInfo( new Mesh(new CylinderGeometry(0.2, 0, 0.6, 4), matInvisible), new double[]{ 0, - 0.3, 0 }, new double[]{ 0, 0, Math.PI})
                            }
                        },
                        {"Z", new ListEx<ControlInfo>
                            {
                                new ControlInfo( new Mesh(new CylinderGeometry(0.2, 0, 0.6, 4), matInvisible), new double[]{ 0, 0, 0.3 }, new double[]{Math.PI / 2, 0, 0 }),
                                new ControlInfo( new Mesh(new CylinderGeometry(0.2, 0, 0.6, 4), matInvisible), new double[]{ 0, 0, - 0.3 }, new double[]{ - Math.PI / 2, 0, 0 })
                            }
                        },

                        {"XY", new ListEx<ControlInfo>
                            {
                                new ControlInfo( new Mesh(new BoxGeometry(0.2, 0.2, 0.01), matInvisible), new double[]{ 0.15, 0.15, 0 }),
                            }
                        },

                        {"YZ", new ListEx<ControlInfo>
                            {
                                new ControlInfo( new Mesh(new BoxGeometry(0.2, 0.2, 0.01), matInvisible), new double[]{ 0, 0.15, 0.15 }, new double[]{ 0, Math.PI / 2, 0 }),
                            }
                        },

                        {"XZ", new ListEx<ControlInfo>
                            {
                                new ControlInfo( new Mesh(new BoxGeometry(0.2, 0.2, 0.01), matInvisible), new double[]{ 0.15, 0, 0.15 }, new double[]{ - Math.PI / 2, 0, 0 }),
                            }
                        },

                        {"XYZ", new ListEx<ControlInfo>
                            {
                                new ControlInfo( new Mesh(new BoxGeometry(0.2, 0.2, 0.2), matInvisible), new double[]{ 0, 0, 0 }),
                            }
                        }
                    };

                var helperScale = new JsObj<string, ListEx<ControlInfo>>
                    {
                        { "X", new ListEx<ControlInfo>
                            {
                                new ControlInfo( new Line(lineGeometry, matHelper.clone()), new double[] { - 1e3, 0, 0 }, null, new double[] { 1e6, 1, 1 }, "helper" )
                            }
                        },
                        { "Y", new ListEx<ControlInfo>
                            {
                                new ControlInfo( new Line(lineGeometry, matHelper.clone()), new double[] { 0, - 1e3, 0 }, new double[] { 0, 0, Math.PI / 2 }, new double[] { 1e6, 1, 1 }, "helper" )
                            }
                        },
                        { "Z", new ListEx<ControlInfo>
                            {
                                new ControlInfo( new Line(lineGeometry, matHelper.clone()), new double[] { 0, 0, - 1e3 }, new double[] { 0, - Math.PI / 2, 0 }, new double[] { 1e6, 1, 1 }, "helper" )
                            }
                        }
                    };

                // Gizmo creation
                this.gizmo = new JsObj<string, Object3D>();
                this.picker = new JsObj<string, Object3D>();
                this.helper = new JsObj<string, Object3D>();

                this.add(this.gizmo["translate"] = setupGizmo(gizmoTranslate));
                this.add(this.gizmo["rotate"] = setupGizmo(gizmoRotate));
                this.add(this.gizmo["scale"] = setupGizmo(gizmoScale));
                this.add(this.picker["translate"] = setupGizmo(pickerTranslate));
                this.add(this.picker["rotate"] = setupGizmo(pickerRotate));
                this.add(this.picker["scale"] = setupGizmo(pickerScale));
                this.add(this.helper["translate"] = setupGizmo(helperTranslate));
                this.add(this.helper["rotate"] = setupGizmo(helperRotate));
                this.add(this.helper["scale"] = setupGizmo(helperScale));

                // Pickers should be hidden always

                this.picker["translate"].visible = false;
                this.picker["rotate"].visible = false;
                this.picker["scale"].visible = false;

            }

            /// <summary>
            /// Creates an Object3D with gizmos described in custom hierarchy definition.
            /// </summary>
            private Object3D setupGizmo(JsObj<string, ListEx<ControlInfo>> gizmoMap)
            {
                var gizmo = new Object3D();
                foreach (var name in gizmoMap.Keys)
                {
                    for (int i = 0; i < gizmoMap[name].Length; i++)
                    {
                        var obj = gizmoMap[name][i].obj3d.clone();
                        var position = gizmoMap[name][i].position;
                        var rotation = gizmoMap[name][i].rotation;
                        var scale = gizmoMap[name][i].scale;
                        var tag = gizmoMap[name][i].tag;

                        // name and tag properties are essential for picking and updating logic.
                        obj.name = name;
                        obj.userData["tag"] = tag;

                        if (position != null)
                        {
                            obj.position.Set(position[0], position[1], position[2]);
                        }

                        if (rotation != null)
                        {
                            obj.rotation.Set(rotation[0], rotation[1], rotation[2]);
                        }

                        if (scale != null)
                        {
                            obj.scale.Set(scale[0], scale[1], scale[2]);
                        }

                        obj.updateMatrix();

                        var tempGeometry = (obj as IGeometry).getGeometry().clone();
                        tempGeometry.applyMatrix4(obj.matrix);
                        (obj as IGeometry).setGeometry(tempGeometry);
                        obj.renderOrder = int.MaxValue;

                        obj.position.Set(0, 0, 0);
                        obj.rotation.Set(0, 0, 0);
                        obj.scale.Set(1, 1, 1);

                        gizmo.add(obj);
                    }
                }
                return gizmo;
            }


            /// <summary>
            /// updateMatrixWorld will update transformations and appearance of individual handles
            /// </summary>
            public override void updateMatrixWorld(bool force = false)
            {
                var space = this.mode == "scale" ? "local" : this.space; // scale always oriented to local rotation

                var quaternion = space == "local" ? this.worldQuaternion : _identityQuaternion;

                // Show only gizmos for current transform mode

                this.gizmo["translate"].visible = this.mode == "translate";
                this.gizmo["rotate"].visible = this.mode == "rotate";
                this.gizmo["scale"].visible = this.mode == "scale";

                this.helper["translate"].visible = this.mode == "translate";
                this.helper["rotate"].visible = this.mode == "rotate";
                this.helper["scale"].visible = this.mode == "scale";


                var handles = new ListEx<Object3D>();
                handles = handles.Concat(this.picker[this.mode].children);
                handles = handles.Concat(this.gizmo[this.mode].children);
                handles = handles.Concat(this.helper[this.mode].children);

                for (int i = 0; i < handles.Length; i++)
                {
                    var handle = handles[i];

                    // hide aligned to camera
                    handle.visible = true;
                    handle.rotation.Set(0, 0, 0);
                    handle.position.Copy(this.worldPosition);

                    double factor = 0;

                    if (this.camera is OrthographicCamera)
                    {
                        var ortCamera = this.camera as OrthographicCamera;
                        factor = (ortCamera.top - ortCamera.bottom) / this.camera.zoom;
                    }
                    else
                    {
                        var perCamera = this.camera as PerspectiveCamera;
                        factor = this.worldPosition.DistanceTo(this.cameraPosition) * Math.Min(1.9 * Math.Tan(Math.PI * perCamera.fov / 360) / perCamera.zoom, 7);
                    }

                    handle.scale.Set(1, 1, 1).MulScalar(factor * this.size / 4.0);

                    // TODO: simplify helpers and consider decoupling from gizmo

                    if (handle.userData.TryGetValue("tag", out object val) && (string)val == "helper")
                    {
                        handle.visible = false;

                        if (handle.name == "AXIS")
                        {
                            handle.visible = this.axis != null;

                            if (this.axis == "X")
                            {
                                _tempQuaternion.SetFromEuler(_tempEuler.Set(0, 0, 0));
                                handle.quaternion.Copy(quaternion).Multiply(_tempQuaternion);

                                if (Math.Abs(_alignVector.Copy(_unitX).ApplyQuaternion(quaternion).Dot(this.eye)) > 0.9)
                                {
                                    handle.visible = false;
                                }
                            }

                            if (this.axis == "Y")
                            {
                                _tempQuaternion.SetFromEuler(_tempEuler.Set(0, 0, Math.PI / 2));
                                handle.quaternion.Copy(quaternion).Multiply(_tempQuaternion);

                                if (Math.Abs(_alignVector.Copy(_unitY).ApplyQuaternion(quaternion).Dot(this.eye)) > 0.9)
                                {
                                    handle.visible = false;
                                }
                            }

                            if (this.axis == "Z")
                            {

                                _tempQuaternion.SetFromEuler(_tempEuler.Set(0, Math.PI / 2, 0));
                                handle.quaternion.Copy(quaternion).Multiply(_tempQuaternion);

                                if (Math.Abs(_alignVector.Copy(_unitZ).ApplyQuaternion(quaternion).Dot(this.eye)) > 0.9)
                                {
                                    handle.visible = false;
                                }
                            }

                            if (this.axis == "XYZE")
                            {
                                _tempQuaternion.SetFromEuler(_tempEuler.Set(0, Math.PI / 2, 0));
                                _alignVector.Copy(this.rotationAxis);
                                handle.quaternion.SetFromRotationMatrix(_lookAtMatrix.LookAt(_zeroVector, _alignVector, _unitY));
                                handle.quaternion.Multiply(_tempQuaternion);
                                handle.visible = this.dragging;
                            }

                            if (this.axis == "E")
                            {
                                handle.visible = false;
                            }
                        }
                        else if (handle.name == "START")
                        {
                            handle.position.Copy(this.worldPositionStart);
                            handle.visible = this.dragging;
                        }
                        else if (handle.name == "END")
                        {
                            handle.position.Copy(this.worldPosition);
                            handle.visible = this.dragging;
                        }
                        else if (handle.name == "DELTA")
                        {
                            handle.position.Copy(this.worldPositionStart);
                            handle.quaternion.Copy(this.worldQuaternionStart);
                            _tempVector.Set(1e-10, 1e-10, 1e-10).Add(this.worldPositionStart).Sub(this.worldPosition).MultiplyScalar(-1);
                            _tempVector.ApplyQuaternion(this.worldQuaternionStart.Clone().Invert());
                            handle.scale.Copy(_tempVector);
                            handle.visible = this.dragging;
                        }
                        else
                        {
                            handle.quaternion.Copy(quaternion);
                            if (this.dragging)
                            {
                                handle.position.Copy(this.worldPositionStart);
                            }
                            else
                            {
                                handle.position.Copy(this.worldPosition);
                            }

                            if (this.axis != null)
                            {
                                handle.visible = this.axis.search(handle.name) != -1;
                            }
                        }

                        // If updating helper, skip rest of the loop
                        continue;
                    }

                    // Align handles to current local or world rotation

                    handle.quaternion.Copy(quaternion);

                    if (this.mode == "translate" || this.mode == "scale")
                    {
                        // Hide translate and scale axis facing the camera

                        var AXIS_HIDE_THRESHOLD = 0.99;
                        var PLANE_HIDE_THRESHOLD = 0.2;

                        if (handle.name == "X")
                        {
                            if (Math.Abs(_alignVector.Copy(_unitX).ApplyQuaternion(quaternion).Dot(this.eye)) > AXIS_HIDE_THRESHOLD)
                            {
                                handle.scale.Set(1e-10, 1e-10, 1e-10);
                                handle.visible = false;
                            }
                        }

                        if (handle.name == "Y")
                        {
                            if (Math.Abs(_alignVector.Copy(_unitY).ApplyQuaternion(quaternion).Dot(this.eye)) > AXIS_HIDE_THRESHOLD)
                            {
                                handle.scale.Set(1e-10, 1e-10, 1e-10);
                                handle.visible = false;
                            }
                        }

                        if (handle.name == "Z")
                        {
                            if (Math.Abs(_alignVector.Copy(_unitZ).ApplyQuaternion(quaternion).Dot(this.eye)) > AXIS_HIDE_THRESHOLD)
                            {
                                handle.scale.Set(1e-10, 1e-10, 1e-10);
                                handle.visible = false;
                            }
                        }

                        if (handle.name == "XY")
                        {
                            if (Math.Abs(_alignVector.Copy(_unitZ).ApplyQuaternion(quaternion).Dot(this.eye)) < PLANE_HIDE_THRESHOLD)
                            {
                                handle.scale.Set(1e-10, 1e-10, 1e-10);
                                handle.visible = false;
                            }
                        }

                        if (handle.name == "YZ")
                        {
                            if (Math.Abs(_alignVector.Copy(_unitX).ApplyQuaternion(quaternion).Dot(this.eye)) < PLANE_HIDE_THRESHOLD)
                            {
                                handle.scale.Set(1e-10, 1e-10, 1e-10);
                                handle.visible = false;
                            }
                        }

                        if (handle.name == "XZ")
                        {
                            if (Math.Abs(_alignVector.Copy(_unitY).ApplyQuaternion(quaternion).Dot(this.eye)) < PLANE_HIDE_THRESHOLD)
                            {
                                handle.scale.Set(1e-10, 1e-10, 1e-10);
                                handle.visible = false;
                            }
                        }
                    }
                    else if (this.mode == "rotate")
                    {
                        // Align handles to current local or world rotation

                        _tempQuaternion2.Copy(quaternion);
                        _alignVector.Copy(this.eye).ApplyQuaternion(_tempQuaternion.Copy(quaternion).Invert());

                        if (handle.name.search("E") != -1)
                        {
                            handle.quaternion.SetFromRotationMatrix(_lookAtMatrix.LookAt(this.eye, _zeroVector, _unitY));
                        }

                        if (handle.name == "X")
                        {
                            _tempQuaternion.SetFromAxisAngle(_unitX, Math.Atan2(-_alignVector.Y, _alignVector.Z));
                            _tempQuaternion.MultiplyQuaternions(_tempQuaternion2, _tempQuaternion);
                            handle.quaternion.Copy(_tempQuaternion);
                        }

                        if (handle.name == "Y")
                        {
                            _tempQuaternion.SetFromAxisAngle(_unitY, Math.Atan2(_alignVector.X, _alignVector.Z));
                            _tempQuaternion.MultiplyQuaternions(_tempQuaternion2, _tempQuaternion);
                            handle.quaternion.Copy(_tempQuaternion);
                        }

                        if (handle.name == "Z")
                        {
                            _tempQuaternion.SetFromAxisAngle(_unitZ, Math.Atan2(_alignVector.Y, _alignVector.X));
                            _tempQuaternion.MultiplyQuaternions(_tempQuaternion2, _tempQuaternion);
                            handle.quaternion.Copy(_tempQuaternion);
                        }
                    }

                    // Hide disabled axes
                    handle.visible = handle.visible && (handle.name.indexOf("X") == -1 || this.showX);
                    handle.visible = handle.visible && (handle.name.indexOf("Y") == -1 || this.showY);
                    handle.visible = handle.visible && (handle.name.indexOf("Z") == -1 || this.showZ);
                    handle.visible = handle.visible && (handle.name.indexOf("E") == -1 || (this.showX && this.showY && this.showZ));

                    // highlight selected axis
                    var matObj = (handle as IMaterialObject).getMaterial();
                    matObj.userData["_color"] = matObj.userData["_color"] ?? matObj.color.Clone();
                    matObj.userData["_opacity"] = matObj.userData["_opacity"] ?? matObj.opacity;

                    matObj.color.Copy((Color)matObj.userData["_color"]);
                    matObj.opacity = (double)matObj.userData["_opacity"];

                    if (this.enabled && this.axis != null)
                    {
                        if (handle.name == this.axis)
                        {
                            matObj.color.SetHex(0xffff00);
                            matObj.opacity = 1.0;
                        }
                        else if (this.axis.split("").Some((a) =>
                        {
                            return handle.name == a;
                        }))
                        {
                            matObj.color.SetHex(0xffff00);
                            matObj.opacity = 1.0;
                        }
                    }
                }

                base.updateMatrixWorld(force);
            }
        }

        public class TransformControlsPlane : Mesh
        {
            public string space;
            public string mode;
            public string axis;
            public Vector3 worldPosition;
            public Vector3 eye;
            public Quaternion worldQuaternion;
            public Quaternion cameraQuaternion;

            //public Camera camera;
            //public Object3D obj;
            //public bool enabled;
            //public object translationSnap;
            //public object rotationSnap;
            //public object scaleSnap;
            //public double size;
            //public bool dragging;
            //public bool showX;
            //public bool showY;
            //public bool showZ;

            public TransformControlsPlane()
                : base(
                    new PlaneGeometry(100000, 100000, 2, 2),
                    new MeshBasicMaterial
                    {
                        visible = false,
                        wireframe = true,
                        side = Constants.DoubleSide,
                        transparent = true,
                        opacity = 0.1,
                        toneMapped = false
                    }
                )
            {
                this.type = "TransformControlsPlane";
            }
            public override void updateMatrixWorld(bool force = false)
            {
                var space = this.space;
                this.position.Copy(this.worldPosition);
                if (this.mode == "scale")
                    space = "local"; // scale always oriented to local rotation

                _v1.Copy(_unitX).ApplyQuaternion(space == "local" ? this.worldQuaternion : _identityQuaternion);
                _v2.Copy(_unitY).ApplyQuaternion(space == "local" ? this.worldQuaternion : _identityQuaternion);
                _v3.Copy(_unitZ).ApplyQuaternion(space == "local" ? this.worldQuaternion : _identityQuaternion);

                // Align the plane for current transform mode, axis and space.

                _alignVector.Copy(_v2);

                switch (this.mode)
                {
                    case "translate":
                    case "scale":
                        switch (this.axis)
                        {
                            case "X":
                                _alignVector.Copy(this.eye).Cross(_v1);
                                _dirVector.Copy(_v1).Cross(_alignVector);
                                break;
                            case "Y":
                                _alignVector.Copy(this.eye).Cross(_v2);
                                _dirVector.Copy(_v2).Cross(_alignVector);
                                break;
                            case "Z":
                                _alignVector.Copy(this.eye).Cross(_v3);
                                _dirVector.Copy(_v3).Cross(_alignVector);
                                break;
                            case "XY":
                                _dirVector.Copy(_v3);
                                break;
                            case "YZ":
                                _dirVector.Copy(_v1);
                                break;
                            case "XZ":
                                _alignVector.Copy(_v3);
                                _dirVector.Copy(_v2);
                                break;
                            case "XYZ":
                            case "E":
                                _dirVector.Set(0, 0, 0);
                                break;
                        }
                        break;
                    case "rotate":
                    default:
                        // special case for rotate
                        _dirVector.Set(0, 0, 0);
                        break;
                }

                if (_dirVector.Length() == 0)
                {
                    // If in rotate mode, make the plane parallel to camera
                    this.quaternion.Copy(this.cameraQuaternion);
                }
                else
                {
                    _tempMatrix.LookAt(_tempVector.Set(0, 0, 0), _dirVector, _alignVector);
                    this.quaternion.SetFromRotationMatrix(_tempMatrix);
                }
                base.updateMatrixWorld(force);

            }
        }

        private static Raycaster _raycaster = new Raycaster();
        private static Vector3 _tempVector = new Vector3();
        private static Vector3 _tempVector2 = new Vector3();
        private static Quaternion _tempQuaternion = new Quaternion();
        private static Unit _unit = new Unit()
        {
            X = new Vector3(1, 0, 0),
            Y = new Vector3(0, 1, 0),
            Z = new Vector3(0, 0, 1)
        };

        private static EventArgs _changeEvent = new EventArgs { type = "change" };
        private static EventArgs _mouseDownEvent = new EventArgs { type = "mouseDown" };
        private static EventArgs _mouseUpEvent = new EventArgs { type = "mouseUp", mode = null };
        private static EventArgs _objectChangeEvent = new EventArgs { type = "objectChange" };

        public GLControl glControl;
        public TransformControlsGizmo _gizmo;
        public TransformControlsPlane _plane;

        public Vector3 _offset;
        public Vector3 _startNorm;
        public Vector3 _endNorm;
        public Vector3 _cameraScale;

        public Vector3 _parentPosition;
        public Quaternion _parentQuaternion;
        public Quaternion _parentQuaternionInv;
        public Vector3 _parentScale;

        public Vector3 _worldScaleStart;
        public Quaternion _worldQuaternionInv;
        public Vector3 _worldScale;

        public Vector3 _positionStart;
        public Quaternion _quaternionStart;
        public Vector3 _scaleStart;

        #region Props
        private Camera _cameraDefault;
        private Camera _camera;
        public Camera camera
        {
            get => _camera ?? _cameraDefault;
            set
            {
                if (value != _camera)
                {
                    _camera = value;
                    //this._plane.camera = value;
                    this._gizmo.camera = value;
                    this.dispatchEvent(new EventArgs { type = nameof(camera) + "-changed", value = value });
                    this.dispatchEvent(_changeEvent);
                }
            }
        }

        private Object3D _objDefault;
        private Object3D _obj;
        public Object3D obj
        {
            get => _obj ?? _objDefault;
            set
            {
                if (value != _obj)
                {
                    _obj = value;
                    //this._plane.obj = value;
                    //this._gizmo.obj = value;
                    this.dispatchEvent(new EventArgs { type = nameof(obj) + "-changed", value = value });
                    this.dispatchEvent(_changeEvent);
                }
            }
        }

        private bool _enabledDefault;
        private bool? _enabled;
        public bool enabled
        {
            get => _enabled ?? _enabledDefault;
            set
            {
                if (value != _enabled)
                {
                    _enabled = value;
                    //this._plane.enabled = value;
                    this._gizmo.enabled = value;

                    this.dispatchEvent(new EventArgs { type = nameof(enabled) + "-changed", value = value });
                    this.dispatchEvent(_changeEvent);
                }
            }
        }

        private string _axisDefault;
        private string _axis;
        public string axis
        {
            get => _axis ?? _axisDefault;
            set
            {
                if (value != _axis)
                {
                    _axis = value;
                    this._plane.axis = value;
                    this._gizmo.axis = value;
                    this.dispatchEvent(new EventArgs { type = nameof(axis) + "-changed", value = value });
                    this.dispatchEvent(_changeEvent);
                }
            }
        }

        private string _modeDefault;
        private string _mode;
        public string mode
        {
            get => _mode ?? _modeDefault;
            set
            {
                if (value != _mode)
                {
                    _mode = value;
                    this._plane.mode = value;
                    this._gizmo.mode = value;
                    this.dispatchEvent(new EventArgs { type = nameof(mode) + "-changed", value = value });
                    this.dispatchEvent(_changeEvent);
                }
            }
        }

        private double? _translationSnapDefault;
        private double? _translationSnap;
        public double? translationSnap
        {
            get => _translationSnap ?? _translationSnapDefault;
            set
            {
                if (value != _translationSnap)
                {
                    _translationSnap = value;
                    //this._plane.translationSnap = value;
                    //this._gizmo.translationSnap = value;
                    this.dispatchEvent(new EventArgs { type = nameof(translationSnap) + "-changed", value = value });
                    this.dispatchEvent(_changeEvent);
                }
            }
        }

        private double? _rotationSnapDefault;
        private double? _rotationSnap;
        public double? rotationSnap
        {
            get => _rotationSnap ?? _rotationSnapDefault;
            set
            {
                if (value != _rotationSnap)
                {
                    _rotationSnap = value;
                    //this._plane.rotationSnap = value;
                    //this._gizmo.rotationSnap = value;
                    this.dispatchEvent(new EventArgs { type = nameof(rotationSnap) + "-changed", value = value });
                    this.dispatchEvent(_changeEvent);
                }
            }
        }

        private double? _scaleSnapDefault;
        private double? _scaleSnap;
        public double? scaleSnap
        {
            get => _scaleSnap ?? _scaleSnapDefault;
            set
            {
                if (value != _scaleSnap)
                {
                    _scaleSnap = value;
                    //this._plane.scaleSnap = value;
                    //this._gizmo.scaleSnap = value;
                    this.dispatchEvent(new EventArgs { type = nameof(scaleSnap) + "-changed", value = value });
                    this.dispatchEvent(_changeEvent);
                }
            }
        }

        private string _spaceDefault;
        private string _space;
        public string space
        {
            get => _space ?? _spaceDefault;
            set
            {
                if (value != _space)
                {
                    _space = value;
                    this._plane.space = value;
                    this._gizmo.space = value;
                    this.dispatchEvent(new EventArgs { type = nameof(space) + "-changed", value = value });
                    this.dispatchEvent(_changeEvent);
                }
            }
        }

        private double _sizeDefault;
        private double? _size;
        public double size
        {
            get => _size ?? _sizeDefault;
            set
            {
                if (value != _size)
                {
                    _size = value;
                    //this._plane.size = value;
                    this._gizmo.size = value;
                    this.dispatchEvent(new EventArgs { type = nameof(size) + "-changed", value = value });
                    this.dispatchEvent(_changeEvent);
                }
            }
        }

        private bool _draggingDefault;
        private bool? _dragging;
        public bool dragging
        {
            get => _dragging ?? _draggingDefault;
            set
            {
                if (value != _dragging)
                {
                    _dragging = value;
                    //this._plane.dragging = value;
                    this._gizmo.dragging = value;
                    this.dispatchEvent(new EventArgs { type = nameof(dragging) + "-changed", value = value });
                    this.dispatchEvent(_changeEvent);
                }
            }
        }

        private bool _showXDefault;
        private bool? _showX;
        public bool showX
        {
            get => _showX ?? _showXDefault;
            set
            {
                if (value != _showX)
                {
                    _showX = value;
                    //this._plane.showX = value;
                    this._gizmo.showX = value;
                    this.dispatchEvent(new EventArgs { type = nameof(showX) + "-changed", value = value });
                    this.dispatchEvent(_changeEvent);
                }
            }
        }

        private bool _showYDefault;
        private bool? _showY;
        public bool showY
        {
            get => _showY ?? _showYDefault;
            set
            {
                if (value != _showY)
                {
                    _showY = value;
                    //this._plane.showY = value;
                    this._gizmo.showY = value;
                    this.dispatchEvent(new EventArgs { type = nameof(showY) + "-changed", value = value });
                    this.dispatchEvent(_changeEvent);
                }
            }
        }

        private bool _showZDefault;
        private bool? _showZ;
        public bool showZ
        {
            get => _showZ ?? _showZDefault;
            set
            {
                if (value != _showZ)
                {
                    _showZ = value;
                    //this._plane.showZ = value;
                    this._gizmo.showZ = value;
                    this.dispatchEvent(new EventArgs { type = nameof(showZ) + "-changed", value = value });
                    this.dispatchEvent(_changeEvent);
                }
            }
        }

        private Vector3 _worldPositionDefault;
        private Vector3 _worldPosition;
        public Vector3 worldPosition
        {
            get => _worldPosition ?? _worldPositionDefault;
            set
            {
                if (value != _worldPosition)
                {
                    _worldPosition = value;
                    this._plane.worldPosition = value;
                    this._gizmo.worldPosition = value;
                    this.dispatchEvent(new EventArgs { type = nameof(worldPosition) + "-changed", value = value });
                    this.dispatchEvent(_changeEvent);
                }
            }
        }

        private Vector3 _worldPositionStartDefault;
        private Vector3 _worldPositionStart;
        public Vector3 worldPositionStart
        {
            get => _worldPositionStart ?? _worldPositionStartDefault;
            set
            {
                if (value != _worldPositionStart)
                {
                    _worldPositionStart = value;
                    //this._plane.worldPositionStart = value;
                    this._gizmo.worldPositionStart = value;
                    this.dispatchEvent(new EventArgs { type = nameof(worldPositionStart) + "-changed", value = value });
                    this.dispatchEvent(_changeEvent);
                }
            }
        }

        private Quaternion _worldQuaternionDefault;
        private Quaternion _worldQuaternion;
        public Quaternion worldQuaternion
        {
            get => _worldQuaternion ?? _worldQuaternionDefault;
            set
            {
                if (value != _worldQuaternion)
                {
                    _worldQuaternion = value;
                    this._plane.worldQuaternion = value;
                    this._gizmo.worldQuaternion = value;
                    this.dispatchEvent(new EventArgs { type = nameof(worldQuaternion) + "-changed", value = value });
                    this.dispatchEvent(_changeEvent);
                }
            }
        }

        private Quaternion _worldQuaternionStartDefault;
        private Quaternion _worldQuaternionStart;
        public Quaternion worldQuaternionStart
        {
            get => _worldQuaternionStart ?? _worldQuaternionStartDefault;
            set
            {
                if (value != _worldQuaternionStart)
                {
                    _worldQuaternionStart = value;
                    //this._plane.worldQuaternionStart = value;
                    this._gizmo.worldQuaternionStart = value;
                    this.dispatchEvent(new EventArgs { type = nameof(worldQuaternionStart) + "-changed", value = value });
                    this.dispatchEvent(_changeEvent);
                }
            }
        }

        private Vector3 _cameraPositionDefault;
        private Vector3 _cameraPosition;
        public Vector3 cameraPosition
        {
            get => _cameraPosition ?? _cameraPositionDefault;
            set
            {
                if (value != _cameraPosition)
                {
                    _cameraPosition = value;
                    //this._plane.cameraPosition = value;
                    this._gizmo.cameraPosition = value;
                    this.dispatchEvent(new EventArgs { type = nameof(cameraPosition) + "-changed", value = value });
                    this.dispatchEvent(_changeEvent);
                }
            }
        }

        private Quaternion _cameraQuaternionDefault;
        private Quaternion _cameraQuaternion;
        public Quaternion cameraQuaternion
        {
            get => _cameraQuaternion ?? _cameraQuaternionDefault;
            set
            {
                if (value != _cameraQuaternion)
                {
                    _cameraQuaternion = value;
                    this._plane.cameraQuaternion = value;
                    //this._gizmo.cameraQuaternion = value;
                    this.dispatchEvent(new EventArgs { type = nameof(cameraQuaternion) + "-changed", value = value });
                    this.dispatchEvent(_changeEvent);
                }
            }
        }

        private Vector3 _pointStartDefault;
        private Vector3 _pointStart;
        public Vector3 pointStart
        {
            get => _pointStart ?? _pointStartDefault;
            set
            {
                if (value != _pointStart)
                {
                    _pointStart = value;
                    //this._plane.pointStart = value;
                    //this._gizmo.pointStart = value;
                    this.dispatchEvent(new EventArgs { type = nameof(pointStart) + "-changed", value = value });
                    this.dispatchEvent(_changeEvent);
                }
            }
        }

        private Vector3 _pointEndDefault;
        private Vector3 _pointEnd;
        public Vector3 pointEnd
        {
            get => _pointEnd ?? _pointEndDefault;
            set
            {
                if (value != _pointEnd)
                {
                    _pointEnd = value;
                    //this._plane.pointEnd = value;
                    //this._gizmo.pointEnd = value;
                    this.dispatchEvent(new EventArgs { type = nameof(pointEnd) + "-changed", value = value });
                    this.dispatchEvent(_changeEvent);
                }
            }
        }

        private Vector3 _rotationAxisDefault;
        private Vector3 _rotationAxis;
        public Vector3 rotationAxis
        {
            get => _rotationAxis ?? _rotationAxisDefault;
            set
            {
                if (value != _rotationAxis)
                {
                    _rotationAxis = value;
                    //this._plane.rotationAxis = value;
                    this._gizmo.rotationAxis = value;
                    this.dispatchEvent(new EventArgs { type = nameof(rotationAxis) + "-changed", value = value });
                    this.dispatchEvent(_changeEvent);
                }
            }
        }

        private double _rotationAngleDefault;
        private double? _rotationAngle;
        public double rotationAngle
        {
            get => _rotationAngle ?? _rotationAngleDefault;
            set
            {
                if (value != _rotationAngle)
                {
                    _rotationAngle = value;
                    //this._plane.rotationAngle = value;
                    //this._gizmo.rotationAngle = value;
                    this.dispatchEvent(new EventArgs { type = nameof(rotationAngle) + "-changed", value = value });
                    this.dispatchEvent(_changeEvent);
                }
            }
        }

        private Vector3 _eyeDefault;
        private Vector3 _eye;
        public Vector3 eye
        {
            get => _eye ?? _eyeDefault;
            set
            {
                if (value != _eye)
                {
                    _eye = value;
                    this._plane.eye = value;
                    this._gizmo.eye = value;
                    this.dispatchEvent(new EventArgs { type = nameof(eye) + "-changed", value = value });
                    this.dispatchEvent(_changeEvent);
                }
            }
        }
        #endregion Props
        public TransformControls(Camera camera, GLControl domElement)
        {
            if (domElement == null)
            {
                console.warn("THREE.TransformControls: The second parameter \"domElement\" is now mandatory.");
                //domElement = document;
            }

            this.visible = false;
            this.glControl = domElement;
            //this.domElement.style.touchAction = 'none'; // disable touch scroll

            var _gizmo = new TransformControlsGizmo();
            this._gizmo = _gizmo;
            this.add(_gizmo);

            var _plane = new TransformControlsPlane();
            this._plane = _plane;
            this.add(_plane);

            // Define properties with getters/setter
            // Setting the defined property will automatically trigger change event
            // Defined properties are passed down to gizmo and plane

            defineProperty("camera", camera);
            defineProperty("object", null);
            defineProperty("enabled", true);
            defineProperty("axis", null);
            defineProperty("mode", "translate");
            defineProperty("translationSnap", null);
            defineProperty("rotationSnap", null);
            defineProperty("scaleSnap", null);
            defineProperty("space", "world");
            defineProperty("size", 1);
            defineProperty("dragging", false);
            defineProperty("showX", true);
            defineProperty("showY", true);
            defineProperty("showZ", true);

            // Reusable utility variables

            var worldPosition = new Vector3();
            var worldPositionStart = new Vector3();
            var worldQuaternion = new Quaternion();
            var worldQuaternionStart = new Quaternion();
            var cameraPosition = new Vector3();
            var cameraQuaternion = new Quaternion();
            var pointStart = new Vector3();
            var pointEnd = new Vector3();
            var rotationAxis = new Vector3();
            var rotationAngle = 0;
            var eye = new Vector3();

            // TODO: remove properties unused in plane and gizmo

            defineProperty("worldPosition", worldPosition);
            defineProperty("worldPositionStart", worldPositionStart);
            defineProperty("worldQuaternion", worldQuaternion);
            defineProperty("worldQuaternionStart", worldQuaternionStart);
            defineProperty("cameraPosition", cameraPosition);
            defineProperty("cameraQuaternion", cameraQuaternion);
            defineProperty("pointStart", pointStart);
            defineProperty("pointEnd", pointEnd);
            defineProperty("rotationAxis", rotationAxis);
            defineProperty("rotationAngle", rotationAngle);
            defineProperty("eye", eye);

            this._offset = new Vector3();
            this._startNorm = new Vector3();
            this._endNorm = new Vector3();
            this._cameraScale = new Vector3();

            this._parentPosition = new Vector3();
            this._parentQuaternion = new Quaternion();
            this._parentQuaternionInv = new Quaternion();
            this._parentScale = new Vector3();

            this._worldScaleStart = new Vector3();
            this._worldQuaternionInv = new Quaternion();
            this._worldScale = new Vector3();

            this._positionStart = new Vector3();
            this._quaternionStart = new Quaternion();
            this._scaleStart = new Vector3();

            //this._getPointer = getPointer.bind(this);
            //this._onPointerDown = onPointerDown.bind(this);
            //this._onPointerHover = onPointerHover.bind(this);
            //this._onPointerMove = onPointerMove.bind(this);
            //this._onPointerUp = onPointerUp.bind(this);

            this.glControl.MouseDown += this.onPointerDown;
            this.glControl.MouseMove += this.onPointerHover;
            this.glControl.MouseUp += this.onPointerUp;
        }

        /// <summary>
        /// Defined getter, setter and store for a property
        /// </summary>
        public void defineProperty(string propName, object defaultValue)
        {
            var propValue = defaultValue;
            switch (propName)
            {
                case "camera":
                    this._cameraDefault = propValue as Camera;
                    this.camera = this._cameraDefault;
                    break;
                case "object":
                    this._objDefault = propValue as Object3D;
                    this.obj = _objDefault;
                    break;
                case "enabled":
                    this._enabledDefault = Convert.ToBoolean(propValue);
                    this.enabled = _enabledDefault;
                    break;
                case "axis":
                    this._axisDefault = (string)propValue;
                    this.axis = _axisDefault;
                    break;
                case "mode":
                    this._modeDefault = (string)propValue;
                    this.mode = _modeDefault;
                    break;
                case "translationSnap":
                    if (propValue != null)
                    {
                        this._translationSnapDefault = Convert.ToDouble(propValue);
                        this.translationSnap = _translationSnapDefault;
                    }
                    break;
                case "rotationSnap":
                    if (propValue != null)
                    {
                        this._rotationSnapDefault = Convert.ToDouble(propValue);
                        this.rotationSnap = _rotationSnapDefault;
                    }
                    break;
                case "scaleSnap":
                    if (propValue != null)
                    {
                        this._scaleSnapDefault = Convert.ToDouble(propValue);
                        this.scaleSnap = _scaleSnapDefault;
                    }
                    break;
                case "space":
                    this._spaceDefault = (string)propValue;
                    this.space = _spaceDefault;
                    break;
                case "size":
                    this._sizeDefault = Convert.ToDouble(propValue);
                    this.size = _sizeDefault;
                    break;
                case "dragging":
                    this._draggingDefault = Convert.ToBoolean(propValue);
                    this.dragging = _draggingDefault;
                    break;
                case "showX":
                    this._showXDefault = Convert.ToBoolean(propValue);
                    this.showX = _showXDefault;
                    break;
                case "showY":
                    this._showYDefault = Convert.ToBoolean(propValue);
                    this.showY = _showXDefault;
                    break;
                case "showZ":
                    this._showZDefault = Convert.ToBoolean(propValue);
                    this.showZ = _showZDefault;
                    break;
                case "worldPosition":
                    this._worldPositionDefault = propValue as Vector3;
                    this.worldPosition = _worldPositionDefault;
                    break;
                case "worldPositionStart":
                    this._worldPositionStartDefault = propValue as Vector3;
                    this.worldPositionStart = _worldPositionStartDefault;
                    break;
                case "worldQuaternion":
                    this._worldQuaternionDefault = propValue as Quaternion;
                    this.worldQuaternion = _worldQuaternionDefault;
                    break;
                case "worldQuaternionStart":
                    this._worldQuaternionStartDefault = propValue as Quaternion;
                    this.worldQuaternionStart = _worldQuaternionStartDefault;
                    break;
                case "cameraPosition":
                    this._cameraPositionDefault = propValue as Vector3;
                    this.cameraPosition = _cameraPositionDefault;
                    break;
                case "cameraQuaternion":
                    this._cameraQuaternionDefault = propValue as Quaternion;
                    this.cameraQuaternion = _cameraQuaternionDefault;
                    break;
                case "pointStart":
                    this._pointStartDefault = propValue as Vector3;
                    this.pointStart = _pointStartDefault;
                    break;
                case "pointEnd":
                    this._pointEndDefault = propValue as Vector3;
                    this.pointEnd = _pointEndDefault;
                    break;
                case "rotationAxis":
                    this._rotationAxisDefault = propValue as Vector3;
                    this.rotationAxis = _rotationAxisDefault;
                    break;
                case "rotationAngle":
                    this._rotationAngleDefault = Convert.ToDouble(propValue);
                    this.rotationAngle = _rotationAngleDefault;
                    break;
                case "eye":
                    this._eyeDefault = propValue as Vector3;
                    this.eye = _eyeDefault;
                    break;
                default:
                    break;
            }
        }

        public override void updateMatrixWorld(bool force = false)
        {
            if (this.obj != null)
            {
                this.obj.updateMatrixWorld();

                if (this.obj.parent == null)
                {
                    console.error("TransformControls: The attached 3D object must be a part of the scene graph.");
                }
                else
                {
                    this.obj.parent.matrixWorld.decompose(this._parentPosition, this._parentQuaternion, this._parentScale);
                }

                this.obj.matrixWorld.decompose(this.worldPosition, this.worldQuaternion, this._worldScale);

                this._parentQuaternionInv.Copy(this._parentQuaternion).Invert();
                this._worldQuaternionInv.Copy(this.worldQuaternion).Invert();
            }

            this.camera.updateMatrixWorld();
            this.camera.matrixWorld.decompose(this.cameraPosition, this.cameraQuaternion, this._cameraScale);

            if (this.camera is OrthographicCamera)
            {
                this.camera.getWorldDirection(this.eye).Negate();
            }
            else
            {
                this.eye.Copy(this.cameraPosition).Sub(this.worldPosition).Normalize();
            }
            base.updateMatrixWorld(force);
        }

        private void pointerHover(Pointer pointer)
        {
            if (this.obj == null || this.dragging)
                return;

            _raycaster.setFromCamera(pointer.ToV2(), this.camera);

            if (intersectObjectWithRay(this._gizmo.picker[this.mode], _raycaster, out Raycaster.Intersection intersect))
            {
                this.axis = intersect.target.name;
            }
            else
            {
                this.axis = null;
            }
        }

        private void pointerDown(Pointer pointer)
        {
            if (this.obj == null || this.dragging == true || pointer.button != MouseButtons.Left)
                return;

            if (this.axis != null)
            {
                _raycaster.setFromCamera(pointer.ToV2(), this.camera);

                if (intersectObjectWithRay(this._plane, _raycaster, out Raycaster.Intersection planeIntersect, true))
                {
                    this.obj.updateMatrixWorld();
                    this.obj.parent.updateMatrixWorld();

                    this._positionStart.Copy(this.obj.position);
                    this._quaternionStart.Copy(this.obj.quaternion);
                    this._scaleStart.Copy(this.obj.scale);

                    this.obj.matrixWorld.decompose(this.worldPositionStart, this.worldQuaternionStart, this._worldScaleStart);

                    this.pointStart.Copy(planeIntersect.point).Sub(this.worldPositionStart);
                }

                this.dragging = true;
                _mouseDownEvent.mode = this.mode;
                this.dispatchEvent(_mouseDownEvent);
            }
        }

        private void pointerMove(Pointer pointer)
        {
            var axis = this.axis;
            var mode = this.mode;
            var obj = this.obj;
            var space = this.space;

            if (mode == "scale")
            {
                space = "local";
            }
            else if (axis == "E" || axis == "XYZE" || axis == "XYZ")
            {
                space = "world";
            }

            if (obj == null || axis == null || this.dragging == false)// || pointer.button != -1)
                return;

            _raycaster.setFromCamera(pointer.ToV2(), this.camera);

            if (!intersectObjectWithRay(this._plane, _raycaster, out Raycaster.Intersection planeIntersect, true))
                return;

            this.pointEnd.Copy(planeIntersect.point).Sub(this.worldPositionStart);

            if (mode == "translate")
            {
                // Apply translate

                this._offset.Copy(this.pointEnd).Sub(this.pointStart);

                if (space == "local" && axis != "XYZ")
                {
                    this._offset.ApplyQuaternion(this._worldQuaternionInv);
                }

                if (axis.indexOf("X") == -1) this._offset.X = 0;
                if (axis.indexOf("Y") == -1) this._offset.Y = 0;
                if (axis.indexOf("Z") == -1) this._offset.Z = 0;

                if (space == "local" && axis != "XYZ")
                {
                    this._offset.ApplyQuaternion(this._quaternionStart).Divide(this._parentScale);
                }
                else
                {
                    this._offset.ApplyQuaternion(this._parentQuaternionInv).Divide(this._parentScale);
                }

                obj.position.Copy(this._offset).Add(this._positionStart);

                // Apply translation snap

                if (this.translationSnap != null)
                {
                    var translationSnap = (double)this.translationSnap;
                    if (space == "local")
                    {
                        obj.position.ApplyQuaternion(_tempQuaternion.Copy(this._quaternionStart).Invert());

                        if (axis.search("X") != -1)
                        {
                            obj.position.X = Math.Round(obj.position.X / translationSnap) * translationSnap;
                        }

                        if (axis.search("Y") != -1)
                        {
                            obj.position.Y = Math.Round(obj.position.Y / translationSnap) * translationSnap;
                        }

                        if (axis.search("Z") != -1)
                        {
                            obj.position.Z = Math.Round(obj.position.Z / translationSnap) * translationSnap;
                        }

                        obj.position.ApplyQuaternion(this._quaternionStart);

                    }

                    if (space == "world")
                    {
                        if (obj.parent != null)
                        {
                            obj.position.Add(_tempVector.SetFromMatrixPosition(obj.parent.matrixWorld));
                        }

                        if (axis.search("X") != -1)
                        {
                            obj.position.X = Math.Round(obj.position.X / translationSnap) * translationSnap;
                        }

                        if (axis.search("Y") != -1)
                        {
                            obj.position.Y = Math.Round(obj.position.Y / translationSnap) * translationSnap;
                        }

                        if (axis.search("Z") != -1)
                        {
                            obj.position.Z = Math.Round(obj.position.Z / translationSnap) * translationSnap;
                        }

                        if (obj.parent != null)
                        {
                            obj.position.Sub(_tempVector.SetFromMatrixPosition(obj.parent.matrixWorld));
                        }
                    }
                }
            }
            else if (mode == "scale")
            {

                if (axis.search("XYZ") != -1)
                {
                    var d = this.pointEnd.Length() / this.pointStart.Length();

                    if (this.pointEnd.Dot(this.pointStart) < 0) d *= -1;

                    _tempVector2.Set(d, d, d);
                }
                else
                {

                    _tempVector.Copy(this.pointStart);
                    _tempVector2.Copy(this.pointEnd);

                    _tempVector.ApplyQuaternion(this._worldQuaternionInv);
                    _tempVector2.ApplyQuaternion(this._worldQuaternionInv);

                    _tempVector2.Divide(_tempVector);

                    if (axis.search("X") == -1)
                    {
                        _tempVector2.X = 1;
                    }

                    if (axis.search("Y") == -1)
                    {
                        _tempVector2.Y = 1;
                    }

                    if (axis.search("Z") == -1)
                    {
                        _tempVector2.Z = 1;
                    }
                }

                // Apply scale

                obj.scale.Copy(this._scaleStart).Multiply(_tempVector2);

                if (this.scaleSnap != null)
                {
                    var scaleSnap = (double)this.scaleSnap;
                    if (axis.search("X") != -1)
                    {
                        obj.scale.X = Math.Round(obj.scale.X / scaleSnap) * scaleSnap;// || scaleSnap;
                    }

                    if (axis.search("Y") != -1)
                    {
                        obj.scale.Y = Math.Round(obj.scale.Y / scaleSnap) * scaleSnap;// || scaleSnap;
                    }

                    if (axis.search("Z") != -1)
                    {
                        obj.scale.Z = Math.Round(obj.scale.Z / scaleSnap) * scaleSnap;// || scaleSnap;
                    }
                }
            }
            else if (mode == "rotate")
            {
                this._offset.Copy(this.pointEnd).Sub(this.pointStart);

                var ROTATION_SPEED = 20 / this.worldPosition.DistanceTo(_tempVector.SetFromMatrixPosition(this.camera.matrixWorld));

                if (axis == "E")
                {
                    this.rotationAxis.Copy(this.eye);
                    this.rotationAngle = this.pointEnd.AngleTo(this.pointStart);

                    this._startNorm.Copy(this.pointStart).Normalize();
                    this._endNorm.Copy(this.pointEnd).Normalize();

                    this.rotationAngle *= (this._endNorm.Cross(this._startNorm).Dot(this.eye) < 0 ? 1 : -1);
                }
                else if (axis == "XYZE")
                {
                    this.rotationAxis.Copy(this._offset).Cross(this.eye).Normalize();
                    this.rotationAngle = this._offset.Dot(_tempVector.Copy(this.rotationAxis).Cross(this.eye)) * ROTATION_SPEED;
                }
                else if (axis == "X" || axis == "Y" || axis == "Z")
                {
                    this.rotationAxis.Copy(_unit[axis]);

                    _tempVector.Copy(_unit[axis]);

                    if (space == "local")
                    {

                        _tempVector.ApplyQuaternion(this.worldQuaternion);

                    }

                    this.rotationAngle = this._offset.Dot(_tempVector.Cross(this.eye).Normalize()) * ROTATION_SPEED;

                }

                // Apply rotation snap

                if (this.rotationSnap != null)
                {
                    var rotationSnap = (double)this.rotationSnap;
                    this.rotationAngle = Math.Round(this.rotationAngle / rotationSnap) * rotationSnap;
                }

                // Apply rotate
                if (space == "local" && axis != "E" && axis != "XYZE")
                {

                    obj.quaternion.Copy(this._quaternionStart);
                    obj.quaternion.Multiply(_tempQuaternion.SetFromAxisAngle(this.rotationAxis, this.rotationAngle)).Normalize();

                }
                else
                {

                    this.rotationAxis.ApplyQuaternion(this._parentQuaternionInv);
                    obj.quaternion.Copy(_tempQuaternion.SetFromAxisAngle(this.rotationAxis, this.rotationAngle));
                    obj.quaternion.Multiply(this._quaternionStart).Normalize();

                }

            }

            this.dispatchEvent(_changeEvent);
            this.dispatchEvent(_objectChangeEvent);

        }

        private void pointerUp(Pointer pointer)
        {
            if (pointer.button != MouseButtons.Left)
                return;

            if (this.dragging && (this.axis != null))
            {
                _mouseUpEvent.mode = this.mode;
                this.dispatchEvent(_mouseUpEvent);
            }

            this.dragging = false;
            this.axis = null;
        }

        public void dispose()
        {
            this.glControl.MouseDown -= this.onPointerDown;
            this.glControl.MouseMove -= this.onPointerHover;
            this.glControl.MouseMove -= this.onPointerMove;
            this.glControl.MouseMove -= this.onPointerUp;

            this.traverse((child) =>
            {
                if (child is ISolid)
                {
                    (child as ISolid).getGeometry().dispose();
                    (child as ISolid).getMaterial().dispose();
                }
            });
        }


        // Set current object
        public TransformControls attach(Object3D obj)
        {
            this.obj = obj;
            this.visible = true;

            return this;
        }

        // Detach from object
        public TransformControls detach()
        {
            this.obj = null;
            this.visible = false;
            this.axis = null;

            return this;
        }

        public void reset()
        {
            if (!this.enabled)
                return;

            if (this.dragging)
            {
                this.obj.position.Copy(this._positionStart);
                this.obj.quaternion.Copy(this._quaternionStart);
                this.obj.scale.Copy(this._scaleStart);

                this.dispatchEvent(_changeEvent);
                this.dispatchEvent(_objectChangeEvent);

                this.pointStart.Copy(this.pointEnd);
            }
        }

        public Raycaster getRaycaster()
        {
            return _raycaster;
        }

        // TODO: deprecate

        public string getMode()
        {
            return this.mode;
        }

        public void setMode(string mode)
        {
            this.mode = mode;
        }

        public void setTranslationSnap(double translationSnap)
        {
            this.translationSnap = translationSnap;
        }

        public void setRotationSnap(double rotationSnap)
        {
            this.rotationSnap = rotationSnap;
        }

        public void setScaleSnap(double scaleSnap)
        {
            this.scaleSnap = scaleSnap;
        }

        public void setSize(double size)
        {
            this.size = size;
        }

        public void setSpace(string space)
        {
            this.space = space;
        }


        // mouse / touch event handlers
        private Pointer getPointer(System.Windows.Forms.MouseEventArgs args)
        {
            var rect = this.glControl;

            return new Pointer()
            {
                x = (args.X - rect.Left) / (double)rect.Width * 2 - 1,
                y = -(args.Y - rect.Top) / (double)rect.Height * 2 + 1,
                button = args.Button
            };
        }

        private void onPointerHover(object sender, MouseEventArgs args)
        {
            if (!this.enabled) return;

            this.pointerHover(this.getPointer(args));
        }
        private void onPointerDown(object sender, MouseEventArgs args)
        {
            if (!this.enabled) return;

            this.glControl.MouseMove += this.onPointerMove;

            this.pointerHover(this.getPointer(args));
            this.pointerDown(this.getPointer(args));
        }
        private void onPointerMove(object sender, MouseEventArgs args)
        {
            if (!this.enabled) return;

            this.pointerMove(this.getPointer(args));
        }

        private void onPointerUp(object sender, MouseEventArgs args)
        {
            if (!this.enabled) return;

            this.glControl.MouseMove -= this.onPointerMove;

            this.pointerUp(this.getPointer(args));
        }

        private bool intersectObjectWithRay(Object3D obj, Raycaster raycaster, out Raycaster.Intersection intersection, bool includeInvisible = false)
        {
            intersection = null;
            var allIntersections = raycaster.intersectObject(obj, true);

            for (int i = 0; i < allIntersections.Length; i++)
            {
                if (allIntersections[i].target.visible || includeInvisible)
                {
                    intersection = allIntersections[i];
                    return true;
                }
            }
            return false;
        }
    }
}